<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="apple-touch-icon" href="apple-touch-icon.png">
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="manifest" href="a3.webmanifest">
    <title>A3 Playground</title>
  </head>
  <body>
    <link rel="stylesheet" href="css/main.css"/>
    <script src="js/Theme.js"></script>
    <script src="js/ammo.js/build/bk/ammo.wasm.js"></script>
    <script src="a3_playground_params.js"></script>
    <div class="div-fullscreen"><div id="container"></div></div>
    <script type="module">
      import * as THREE from "./js/three.js/build/three.module.js";
      import Stats from './jsm/libs/stats.module.js';
		  import { OrbitControls } from './jsm/controls/OrbitControls.js';
      import { GUI } from './jsm/libs/dat.gui.module.js';
      import { ConvexGeometry } from './jsm/geometries/ConvexGeometry.js';
      import { ConvexObjectBreaker } from './jsm/misc/ConvexObjectBreaker.js';
      import * as BufferGeometryUtils from './jsm/utils/BufferGeometryUtils.js';
      // import { initAmmo, AmmoLib } from './js/A3Class.js';
      // import { getMeshShape } from './js/A3Common.js';

      // const
      const AxisX = new THREE.Vector3(1, 0, 0);
      const AxisY = new THREE.Vector3(0, 1, 0);
      const AxisZ = new THREE.Vector3(0, 0, 1);

      const DISABLE_DEACTIVATION = 4;

      // Graphics variables
      let container, stats;
      let camera, controls, scene, renderer;
      let ambientLight;
      let followedCamera;
      let guiCameraPos, guiCameraEuler, guiCameraLookAt;

      let textureLoader;
      const clock = new THREE.Clock();

      // Physics variables
      const gravityConstant = - 9.8;
      let collisionConfiguration;
      let dispatcher;
      let broadphase;
      let solver;
      let softBodySolver;
      let physicsWorld;
      const rigidBodies = [];
      const softBodies = [];
      const margin = 0.05;
      let transformAux1;
      let softBodyHelpers;

      // Objects
      const pos = new THREE.Vector3();
      const quat = new THREE.Quaternion();

      const convexBreaker = new ConvexObjectBreaker();
      const objectsToRemove = [];

      for ( let i = 0; i < 999; i ++ ) {

        objectsToRemove[ i ] = null;

      }

      let numObjectsToRemove = 0;

      const impactPoint = new THREE.Vector3();
      const impactNormal = new THREE.Vector3();

      let ground;
      let objects;
      let followedObject, preFollowedObject, followedList;
      let poolCueBase, poolCue, poolCueRay, poolCueConstraint, poolCueStaus;        
      let startPlatform;
      let startBall;
      let ramp;
      let dominos;
      // 球杆近接传感器
      let tempVRayOrigin, tempVRayDest, tempbtQuat, closestRayResultCallback;
      // 水平碰撞球
      let hHitBridge, hHitBall;
      // 钟摆
      let pendulumBase, pendulumBall, pendulumRope;
      // 滑板
      let slider;
      // 跷跷板
      let seesaw, seesawHinge;
      // 天平
      let balanceBall,balanceConstraint, balanceConstraintA, balanceConstraintB;
      // 漏斗
      let hopper;
      // 小车
      let vehicle, chassisMesh, trunk, vehicleConstraint;
      var FRONT_LEFT = 0;
      var FRONT_RIGHT = 1;
      var BACK_LEFT = 2;
      var BACK_RIGHT = 3;
      var wheelMeshes = [];
      var vehicleCanMove = false;
      var maxEngineForce = 200;
			var maxBreakingForce = 10;
      var maxSpeed = 10;
      // 拱桥
      let bridge;
      // 升降梯
      let lifter, lifterSpeed, liftConstraint;
      // 推土机
      let bulldozerHinge, bulldozer;
      // 气垫
      let cushion;

      // gui
      let gui;

      // await initAmmo();
      // Ammo = AmmoLib;
      
      // tempVRayOrigin = new Ammo.btVector3();
      // tempVRayDest = new Ammo.btVector3();
      // tempbtQuat = new Ammo.btQuaternion();
      // closestRayResultCallback = new Ammo.ClosestRayResultCallback( tempVRayOrigin, tempVRayDest );

      // loadParams();

      // init();

      // animate();

      Ammo().then( function ( AmmoLib ) {

        Ammo = AmmoLib;

        tempVRayOrigin = new Ammo.btVector3();
        tempVRayDest = new Ammo.btVector3();
        tempbtQuat = new Ammo.btQuaternion();
        closestRayResultCallback = new Ammo.ClosestRayResultCallback( tempVRayOrigin, tempVRayDest );

        loadParams();

        init();
        animate();

      } );

      function init() {

        initGraphics();

        initPhysics();

        createObjects();

        initInput();

        setupGUI();

      }

      function reset(){
        clearAllObjects();
        createObjects();
      }

      // 清除所有object
      function clearAllObjects(){
        for(let i = 0, il = objects.length; i < il; i++){
          disposeObject(objects[i]);
        }
        objects = [];
      }
      
      function initGraphics() {

        container = document.getElementById( 'container' );
        
        camera = new THREE.PerspectiveCamera(
          params.cameraFov, window.innerWidth / window.innerHeight, 
          params.cameraNear, params.cameraFar
        );

        followedCamera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);

        scene = new THREE.Scene();
        scene.background = new THREE.Color( params.sceneBackGroundColor );

        var axes = new THREE.AxesHelper(20);
        scene.add(axes);

        camera.position.copy( getPosition(params.cameraPosition) );     

        renderer = new THREE.WebGLRenderer({ alpha: true });
        renderer.setClearColor(0XFFFFFF, 0);
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize( window.innerWidth, window.innerHeight );
        renderer.shadowMap.enabled = true;
        container.appendChild( renderer.domElement );

        controls = new OrbitControls( camera, renderer.domElement );
        const lookAt = getPosition(params.cameraLookAt);  
        controls.target.set( lookAt.x, lookAt.y, lookAt.z );
        controls.update();

        textureLoader = new THREE.TextureLoader();

        ambientLight = new THREE.AmbientLight( params.sceneLight );
        scene.add( ambientLight );

        const light = new THREE.DirectionalLight( 0xffffff, 0.75 );
        light.position.set( 50, 100, -20 );
        light.castShadow = true;
        const d = 200;
        light.shadow.camera.left = - d * 0.2;
        light.shadow.camera.right = d * 0.25;
        light.shadow.camera.top = d * 0.25;
        light.shadow.camera.bottom = - d * 0.25;

        light.shadow.camera.near = d / 30;
        light.shadow.camera.far = d;

        light.shadow.mapSize.x = 1024 * 2;
        light.shadow.mapSize.y = 1024 * 2;

        scene.add( light );

        stats = new Stats();
        stats.domElement.style.position = 'absolute';
        stats.domElement.style.top = '0px';
        container.appendChild( stats.domElement );

        //

        window.addEventListener( 'resize', onWindowResize );

        objects =[];
      }

      function initPhysics() {

        // Physics configuration

        collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
        // collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
        dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
        broadphase = new Ammo.btDbvtBroadphase();
        solver = new Ammo.btSequentialImpulseConstraintSolver();
        softBodySolver = new Ammo.btDefaultSoftBodySolver();
        physicsWorld = new Ammo.btSoftRigidDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration, softBodySolver );
        physicsWorld.setGravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
        physicsWorld.getWorldInfo().set_m_gravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
        physicsWorld.getWorldInfo().set_air_density ( 2.8 );

        // Ammo.btGImpactCollisionAlgorithm.prototype.registerAlgorithm(physicsWorld.getDispatcher());

        transformAux1 = new Ammo.btTransform();

        softBodyHelpers = new Ammo.btSoftBodyHelpers();
      }

      function disposeObject(obj){
        if(obj){
          let children = obj.children;
          if(children){
            for(var i = 0, il = children.length; i < il; i++){
              disposeObject(children[i]);
            }
          }
          let body = obj.userData.physicsBody;
          if(body){
            physicsWorld.removeRigidBody(body);
          }
          if(obj.geometry){
            obj.geometry.dispose();
          }
          if(obj.material){
            obj.material.dispose();
          }
          scene.remove(obj);
        }
      }
      
      function getParam(key, param){
        return params[`${key}${param}`];
      }

      function getPosition(posStr){
        const values = posStr.split(',');
        var pos = [];
        for(var i = 0; i < 3; i++){
          if(i >= values.length || isNaN(parseFloat(values[i]))){
            pos.push(0);
          }
          else{
            pos.push(parseFloat(values[i]));
          }
        }

        return new THREE.Vector3(pos[0], pos[1], pos[2]);
      }

      function getEuler(eulerStr){
        if(!eulerStr){
          return new THREE.Euler();
        }

        const values = eulerStr.split(',');
        var euler = [];
        for(var i = 0; i < 3; i++){
          if(i >= values.length || isNaN(parseFloat(values[i]))){
            euler.push(0);
          }
          else{
            euler.push(parseFloat(values[i]) * Math.PI / 180.0);
          }
        }

        return new THREE.Euler(euler[0], euler[1], euler[2]);
      }

      function getQuaternion(eulerStr){

        var quat = new THREE.Quaternion();
        quat.setFromEuler(getEuler(eulerStr));

        return quat;
      }

      function setBodyUserPointer(obj){
        const btVecUserData = new Ammo.btVector3( 0, 0, 0 );
			  btVecUserData.threeObject = obj;
        obj.userData.physicsBody.setUserPointer( btVecUserData );
      }

      // 地面
      function createGround(){
        const pos = new THREE.Vector3();
        const quat = new THREE.Quaternion();

        disposeObject(ground);

        // Ground
        pos.set( 0, - 0.5, 0 );
        quat.set( 0, 0, 0, 1 );
        ground = createParalellepiped( params.groundSize, 1, params.groundSize, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
        ground.castShadow = true;
        ground.receiveShadow = true;
        textureLoader.load( "textures/grid.png", function ( texture ) {

          texture.wrapS = THREE.RepeatWrapping;
          texture.wrapT = THREE.RepeatWrapping;
          texture.repeat.set(  params.groundSize, params.groundSize );
          ground.material.map = texture;
          ground.material.needsUpdate = true;

        } );

        return ground;
      }

      // 起始平台
      function createPlatform(){

        pos.copy(getPosition(params.startPlatformPosition));
        quat.copy(getQuaternion(params.startPlatformEuler));
        
        const x = params.startPlatformLength;
        const y = params.startPlatformHeight;
        const z = params.startPlatformWidth;
        const material = createMaterial();

        startPlatform = createParalellepiped( x, y, z, 0, pos, quat, material );
        startPlatform.castShadow = true;
        startPlatform.receiveShadow = true;

        return startPlatform;
      }

      // 球杆
      function createPoolCue(){

        pos.copy(startPlatform.position);
        quat.set(0,0,0,1);
        
        const ptHeight = params.startPlatformHeight;
        const ptLength = params.startPlatformLength;

        const r = params.poolCueSize;
        const h = ptLength / 2;
        const mass = params.poolCueMass;        
        const material = createMaterial();

        pos.y += ptHeight / 2 + r;
        pos.x -= ptLength / 2 + h / 2;
        quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);

        poolCueBase = createCylindered( r, h, 0, pos, quat, material );
        poolCueBase.castShadow = true;
        poolCueBase.receiveShadow = true;

        pos.x -= r;       
        poolCue = createCylindered( r*0.8, h, mass, pos, quat, material );

        var cylGeom = new THREE.CylinderGeometry( 0.02, 0.02, ptLength, 6 );
        cylGeom.translate( 0, - 0.5 * ptLength - h / 2, 0 );
        poolCueRay = new THREE.Mesh( cylGeom, new THREE.MeshPhongMaterial( { color: 0x00FF00 } ) );
        poolCue.add(poolCueRay)

        poolCue.castShadow = true;
        poolCue.receiveShadow = true;

        poolCue.userData.direction = 1;
        poolCue.userData.LowerLimit = 0;
        poolCue.userData.UpperLimit = h;

        // 添加约束
        var frameInA = new Ammo.btTransform();
        var frameInB = new Ammo.btTransform();

        frameInA.setIdentity();
        frameInB.setIdentity();
        frameInA.setOrigin(Ammo.btVector3(0.0, 0.0, 0.0));
        frameInB.setOrigin(Ammo.btVector3(0.0, 0.0, 0.0));

        frameInA.getBasis().setEulerZYX(0, 0, Math.PI/2);
        frameInB.getBasis().setEulerZYX(0, 0, Math.PI/2);

        poolCueConstraint = createSliderConstraint(
          poolCueConstraint,
          poolCueBase.userData.physicsBody, poolCue.userData.physicsBody,
          frameInA, frameInB,
          poolCue.userData.LowerLimit, poolCue.userData.UpperLimit
        );

        return poolCue;
      }

      // 起始小球
      function createStartBall(){

        pos.copy(getPosition(params.startPlatformPosition));
        quat.copy(getQuaternion(params.startPlatformEuler));

        const ptHeight = params.startPlatformHeight;
        const r = params.startBallSize;
        const mass = params.startBallMass;
        const friction = params.startBallFriction;
        const restitution = params.startBallRestitution;

        pos.y += ptHeight / 2 + r / 2;

        startBall = createSphered( r, mass, pos, quat, createMaterial() );

        startBall.castShadow = true;
        startBall.receiveShadow = true;

        startBall.userData.physicsBody.setFriction(friction);
        startBall.userData.physicsBody.setRestitution(restitution);
        
        setBodyUserPointer(startBall);

        return startBall;
      }

      // 斜坡
      function createRamp(){
        // 根据平台的高度计算斜坡的长度和高度
        
        pos.copy(getPosition(params.startPlatformPosition));
        quat.copy(getQuaternion(params.startPlatformEuler));
        const ptHeight = params.startPlatformHeight;
        const ptLength = params.startPlatformLength;

        const rz = parseFloat(params.rampRotationZ) * Math.PI / 180.0;
        const h = ptHeight / 2;
        const x = Math.abs(h / Math.sin(rz));
        const y = params.rampHeight;
        const z = params.startPlatformWidth;

        const friction = params.rampFriction;
        
        pos.y += h/2;
        const xOffset = ptLength / 2 - Math.abs(y * Math.sin(rz)) / 2;
        const yOffset = Math.abs(y * Math.cos(rz)) / 2;
        pos.x += Math.abs(x * Math.cos(rz)) / 2 + xOffset;
        pos.y -= yOffset;

        quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), rz);
        
        ramp = createParalellepiped( x, y, z, 0, pos, quat, createMaterial() );
        ramp.castShadow = true;
        ramp.receiveShadow = true;

        ramp.userData.physicsBody.setFriction(friction);

        return ramp;
      }

      // 水平碰撞桥
      function createHHitBridge(){

        pos.copy(ramp.position);
        quat.set(0, 0, 0, 1);

        const ptHeight = params.startPlatformHeight;
        const ptLength = params.startPlatformLength;
        
        const rz = ramp.rotation.z;
        const h = parseFloat(ptHeight) / 2;
        const offsetX = Math.abs(h / Math.tan(rz))/2 ;
        const OffsetY = h / 2;
        const x = params.hHitBridgeLength;
        const y = params.rampHeight;
        const z = params.startPlatformWidth;
        const material = createMaterial();

        pos.x += offsetX + x / 2;
        pos.y -= OffsetY;
        
        hHitBridge = createParalellepiped(x, y, z, 0, pos, quat, material);

        hHitBridge.castShadow = true;
        hHitBridge.receiveShadow = true;

        const pillar1 = new THREE.Mesh( new THREE.BoxGeometry( y, pos.y, z, 1, 1, 1 ), material );
        pillar1.position.set(-x/2 + y/2, -pos.y / 2, 0);
        hHitBridge.add(pillar1);
        const pillar2 = new THREE.Mesh( new THREE.BoxGeometry( y, pos.y, z, 1, 1, 1 ), material );
        pillar2.position.set(x/2-y/2, -pos.y / 2, 0);
        hHitBridge.add(pillar2);

        pillar1.castShadow = true;
        pillar1.receiveShadow = true;
        pillar2.castShadow = true;
        pillar2.receiveShadow = true;

        hHitBridge.userData.physicsBody.setFriction(params.hHitBridgeFriction);
        hHitBridge.userData.physicsBody.setRestitution(params.hHitBridgeRestitution);

        return hHitBridge;
      }

      // 球1：测试水平碰撞
      function createHHitBall(){
        pos.copy(hHitBridge.position);
        quat.set(0, 0, 0, 1);
        const r = params.hHitBallSize;
        const bridgeHeight = params.rampHeight;

        const mass = params.hHitBallMass;
        const friction = params.hHitBallFriction;
        const restitution = params.hHitBallRestitution;

        pos.y += bridgeHeight / 2 + r/2;

        hHitBall = createSphered( r, mass, pos, quat, createMaterial() );


        hHitBall.castShadow = true;
        hHitBall.receiveShadow = true;

        hHitBall.userData.physicsBody.setFriction(friction);
        hHitBall.userData.physicsBody.setRestitution(restitution);

        setBodyUserPointer(hHitBall);
        
        return hHitBall;
      }

      // 钟摆
      function createPendulum(){
        pos.copy(hHitBridge.position);
        quat.set(0, 0, 0, 1);
        var tp = pos.clone();
        const x = params.startPlatformLength;
        const y = params.pendulumHeight;
        const z = params.startPlatformWidth;
        const r = params.pendulumSize;
        const mass = params.pendulumMass;
        const material = createMaterial();
        const restitution = params.pendulumRestitution;
        // 基座
        pos.x += params.hHitBridgeLength / 2 + r * 3;
        // 横梁
        pos.y = y + x / 2;
        pendulumBase = createParalellepiped( x, x, r * 4 + z * 3, 0, pos, quat, material );
        pos.y = y / 2;
        // 立柱
        tp.copy(pos);
        tp.z += z + r * 2;
        const pole1 = createParalellepiped(x, y, z, 0, tp, quat, material);
        tp.copy(pos);
        tp.z -= z + r * 2;
        const pole2 = createParalellepiped(x, y, z, 0, tp, quat, material);
        // 钟摆球
        tp.copy(pos);
        pendulumBall = createSphered(r, mass, tp, quat, material);
        pendulumBall.userData.physicsBody.setRestitution(restitution);
        setBodyUserPointer(pendulumBall);
        // 连接线
        pendulumRope = createRope(pendulumRope, pendulumBase, pendulumBall, y / 2, r);
      }
      
      // 滑板
      function createSlider(){
        pos.copy(pendulumBall.position);
        quat.set(0, 0, 0, 1);

        const x = params.sliderLength;
        const y = params.rampHeight;
        const z = params.startPlatformWidth;
        const h = params.sliderHeight;
        const r = params.sliderSize;
        const mass = params.sliderMass;
        const friction = params.sliderFriction;
        const restitution = params.sliderRestitution;
        const material = createMaterial();
        pos.x += params.pendulumSize * 4 +  x / 2;
        pos.y = h + y / 2;
        slider = createParalellepiped(x, y, z, mass, pos, quat, material, friction, restitution );
        setBodyUserPointer(slider);
        pos.y = h / 2;
        const sliderBaseM = createParalellepiped(y, h, z, 0, pos, quat, material );
        pos.y = h + y + r / 2;
        const sliderBall = createSphered(r, mass, pos, quat, material, friction, restitution );
        setBodyUserPointer(sliderBall);
        pos.y = h * 0.4;
        pos.x += x / 2;
        const sliderBaseLower = createParalellepiped(y, h*0.8, z, 0, pos, quat, material );
    
      }
      
      // 跷跷板 -- 物体转90度 : Hing constraint
      function createSeesaw(){
        pos.copy(slider.position);
        quat.set(0, 0, 0, 1);
        pos.x += params.sliderLength * 0.66;

        const x = params.startPlatformWidth;
        const y = params.rampHeight;
        const z = params.seesawLength;
        const h = params.seesawHeight;
        const mass = getParam('seesaw', 'Mass');
        const friction = getParam('seesaw', 'Friction');
        const restitution = getParam('seesaw', 'Restitution');
        const material = createMaterial();

        pos.x += x;
        pos.y = h / 2;
        pos.z += z / 2 - x / 2;
        quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2);
        const seesawBase = createCylindered(h/2, x, 0, pos, quat, material );

        quat.set(0, 0, 0, 1);
        pos.y += y / 2;
        seesaw = createParalellepiped(x, y, z, mass, pos, quat, material, friction, restitution);
        setBodyUserPointer(seesaw);
        
        // 添加垫块辅助跷跷板恢复
        pos.y = h * 0.5;
        pos.z += z * 0.5;
        const block = createParalellepiped(h, h, h, 0, pos, quat, material);
        // Hinge constraint to move the arm
        const pivotA = new Ammo.btVector3( h / 2, 0, 0 );
				const pivotB = new Ammo.btVector3( 0, -y / 2, 0 );
				const axisA = new Ammo.btVector3( 0, 1, 0 );
        const axisB = new Ammo.btVector3( -1, 0, 0 );
        seesawHinge = createHingeConstraint(
          seesawHinge, 
          seesawBase.userData.physicsBody, seesaw.userData.physicsBody,
          pivotA, pivotB, axisA, axisB
        );

      }

      // 天平
      function createBalance(){
        pos.copy(seesaw.position);
        quat.set(0, 0, 0, 1);
        pos.z += params.seesawLength / 2;
        const pad = 0.1;
        const w = params.rampHeight;
        const pw = params.startPlatformWidth;
        const l = params.balanceLength;
        const h = params.balanceHeight;
        const hh = h - params.seesawHeight - w;

        const r = getParam('balance', 'Size');
        const mass = getParam('balance', 'Mass');
        const friction = getParam('balance', 'Friction');
        const restitution = getParam('balance', 'Restitution');

        let material = createMaterial();
        pos.y = h / 2;
        pos.z += l / 2 - w;
  
        const balanceBase = createParalellepiped(pw, h, w, 0, pos, quat, material);
 
        pos.y = h;
        const balancePivot = createSphered(w / 2, 0, pos, quat, material);
        pos.y = h + w;
        const crossbeam = createParalellepiped(pw, w, l, mass * 0.25, pos, quat, material, friction, restitution);
        setBodyUserPointer(crossbeam);
        pos.y += w + r / 2 + pad;
        // pos.z += l / 2;
        balanceBall = createSphered(r, mass, pos, quat, material, friction, restitution);
        setBodyUserPointer(balanceBall);

        
        const basePos = crossbeam.position.clone();
        pos.set(basePos.x, basePos.y - hh / 2 - w / 2, basePos.z - l / 2 + w / 2);
        material = createMaterial(0xff0000);
        const hangA = createParalellepiped(pw, hh, w, mass, pos, quat, material, friction, restitution);
        pos.set(basePos.x, basePos.y - hh / 2 - w / 2, basePos.z + l / 2 - w / 2);
        material = createMaterial(0x00ff00);
        const hangB = createParalellepiped(pw, hh, w, mass, pos, quat, material, friction, restitution);
        setBodyUserPointer(hangA);
        // 添加约束

        var frameInA = new Ammo.btTransform();
        var frameInB = new Ammo.btTransform();

        frameInA.setIdentity();
        frameInB.setIdentity();

        frameInA.setOrigin(Ammo.btVector3(0, 0, 0));
        frameInB.setOrigin(Ammo.btVector3(0, 0, 0));

        var lowerSliderLimit = new Ammo.btVector3(0, -hh/2 - w / 2 - pad, -l/2 + w/2); 
        var hiSliderLimit = new Ammo.btVector3(0, -hh/2 - w / 2 + pad, -l/2 + w/ 2); 
        var lowerAngularLimit = new Ammo.btVector3(0,0,0); 
        var hiAngularLimit = new Ammo.btVector3(0,0,0); 

        balanceConstraintA = create6DofConstraint(
          balanceConstraintA,
          crossbeam.userData.physicsBody,
          hangA.userData.physicsBody,
          frameInA, frameInB,
          lowerSliderLimit, hiSliderLimit, lowerAngularLimit, hiAngularLimit
        );

        lowerSliderLimit.setValue(0, -hh/2 - w / 2 - pad, l/2 - w/2); 
        hiSliderLimit.setValue(0, -hh/2 - w / 2 + pad, l/2 - w/2); 

        balanceConstraintB = create6DofConstraint(
          balanceConstraintB,
          crossbeam.userData.physicsBody,
          hangB.userData.physicsBody,
          frameInA, frameInB,
          lowerSliderLimit, hiSliderLimit, lowerAngularLimit, hiAngularLimit
        );

        lowerSliderLimit.setValue(0, w - pad, 0); 
        hiSliderLimit.setValue(0, w + pad, 0); 
        lowerAngularLimit.setValue(-Math.PI * 0.1, 0, 0); 
        hiAngularLimit.setValue(Math.PI * 0.1, 0, 0);

        balanceConstraint = create6DofConstraint(
          balanceConstraint,
          balancePivot.userData.physicsBody,
          crossbeam.userData.physicsBody,
          frameInA, frameInB,
          lowerSliderLimit, hiSliderLimit, lowerAngularLimit, hiAngularLimit
        );
      }

      // 漏斗
      function createHopper(){
        pos.copy(balanceBall.position);
        quat.set(0, 0, 0, 1);
        const dist = getParam('balance', 'Length');

        const tk = getParam('ramp', 'Height');  // tk:thickness
        const h = getParam('balance', 'Height')  * 0.5;
        const w = getParam('hopper', 'Size');
        const offsetY = getParam('hopper', 'Height');

        const pipeSize = w * 3;
        const material = createMaterial();
        const rotation = getParam('hopper', 'Rotation') * Math.PI / 180.0;
        const sinR = Math.sin(rotation);
        const cosR = Math.cos(rotation);

        const hopperHeight = h * 0.6;
        const pipeHeight = h - offsetY;
        pos.z += dist * 0.5  + pipeSize * 1.5 + hopperHeight * cosR;
        pos.y = h + w  * 0.5;
        const basePos = pos.clone();
        
        basePos.y = pipeHeight * 0.5 + offsetY;
        
        hopper = new THREE.Mesh( new THREE.SphereGeometry( w, 24, 24 ), createMaterial() );
        hopper.position.set(basePos.x, offsetY + w / 2, basePos.z);
        scene.add(hopper);
        objects.push(hopper);

        // 管道
        pos.set(basePos.x, basePos.y, basePos.z - pipeSize * 0.5 - tk * 0.5);
        const pipe1 = createParalellepiped(pipeSize, pipeHeight, tk, 0, pos, quat, material);
        
        pos.y += pipeHeight * 0.5 + cosR * hopperHeight * 0.5 - tk * sinR * 0.5;
        pos.z -= sinR * hopperHeight * 0.5 - tk * cosR * 0.5;
        quat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), -rotation);
        const hopper1 = createParalellepiped(pipeSize, hopperHeight, tk, 0, pos, quat, material);

        pos.set(basePos.x, basePos.y, basePos.z + pipeSize  * 0.5 + tk * 0.5);
        quat.set(0, 0, 0, 1);
        const pipe2 = createParalellepiped(pipeSize,pipeHeight, tk, 0, pos, quat, material);

        pos.y += pipeHeight * 0.5 + cosR * hopperHeight * 0.5 - tk * sinR * 0.5;
        pos.z += sinR * hopperHeight * 0.5 - tk * cosR * 0.5;
        quat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), rotation);
        const hopper2 = createParalellepiped(pipeSize, hopperHeight, tk, 0, pos, quat, material);

        pos.set(basePos.x + pipeSize * 0.5 + tk * 0.5, basePos.y, basePos.z);
        quat.set(0, 0, 0, 1);
        const pipe3 = createParalellepiped(tk, pipeHeight, pipeSize + tk * 2, 0, pos, quat, material);

        pos.y += pipeHeight * 0.5 + cosR * hopperHeight * 0.5 - tk * sinR * 0.5;
        pos.x += sinR * hopperHeight * 0.5 - tk * cosR * 0.5;
        quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), -rotation);
        const hopper3 = createParalellepiped(tk, hopperHeight, pipeSize, 0, pos, quat, material);

        pos.set(basePos.x - pipeSize  * 0.5 - tk  * 0.5, basePos.y + w, basePos.z);
        quat.set(0, 0, 0, 1);
        const pipe4 = createParalellepiped(tk, pipeHeight - w * 2, pipeSize + tk * 2, 0, pos, quat, material);

        pos.y += (pipeHeight - w * 2) * 0.5 + cosR * hopperHeight * 0.5 - tk * sinR * 0.5;
        pos.x -= sinR * hopperHeight * 0.5 - tk * cosR * 0.5;;
        quat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), rotation);
        const hopper4 = createParalellepiped(tk, hopperHeight, pipeSize, 0, pos, quat, material);

        // 补全漏斗：使用顶点坐标添加物体
        var vertices = [];
        var x = hopperHeight * sinR;
        var y = hopperHeight * cosR;
        var z = 0;
        vertices.push(new THREE.Vector3(0, 0, 0));
        vertices.push(new THREE.Vector3(0, 0, 0));
        vertices.push(new THREE.Vector3(x, y, z));
        vertices.push(new THREE.Vector3(x + tk * cosR, y - tk * sinR, z));
        vertices.push(new THREE.Vector3(z, y, x));
        vertices.push(new THREE.Vector3(z, y  - tk * sinR, x + tk * cosR));
        vertices.push(new THREE.Vector3(tk, 0, tk));

        basePos.y += pipeHeight / 2;

        pos.set(basePos.x + pipeSize * 0.5 , basePos.y , basePos.z - pipeSize * 0.5 );
        quat.setFromAxisAngle(AxisY, Math.PI / 2);
        const hopper13 = createConvexObject(vertices, 0, pos, quat, createMaterial());

        pos.set(basePos.x + pipeSize * 0.5 , basePos.y , basePos.z + pipeSize * 0.5 );
        quat.set(0, 0, 0, 1);
        const hopper32 = createConvexObject(vertices, 0, pos, quat, createMaterial());

        pos.set(basePos.x - pipeSize * 0.5 , basePos.y , basePos.z + pipeSize * 0.5 );
        quat.setFromAxisAngle(AxisY, -Math.PI / 2);
        const hopper23 = createConvexObject(vertices, 0, pos, quat, createMaterial());

        pos.set(basePos.x - pipeSize * 0.5 , basePos.y , basePos.z - pipeSize * 0.5 );
        quat.setFromAxisAngle(AxisY, Math.PI);
        const hopper31 = createConvexObject(vertices, 0, pos, quat, createMaterial());
        
      }

      // 小车
      function createVehicle(){
        physicsWorld.removeAction(vehicle);
        vehicleCanMove = false;
        maxSpeed = getParam('vehicle', 'Speed');

        pos.copy(hopper.position);
        quat.setFromAxisAngle(AxisY, -Math.PI / 2);

        const h = getParam('vehicle', 'Height');
        const w = getParam('vehicle', 'Width');
        const l = getParam('vehicle', 'Length');
        const massVehicle = getParam('vehicle', 'Mass');
        const friction = getParam('vehicle', 'Friction');
        const restitution = getParam('vehicle', 'Restitution');

        const chssisHeight = 1;
        const material = createMaterial();

        var chassisWidth = w;
				var chassisHeight = h * 0.125;
				var chassisLength = l;
        var wheelRadiusFront = h * 0.125; // 前轮半径
        var wheelWidthFront = 0.5;  // 前轮宽度
        var wheelAxisFrontPosition = l * 0.5 - wheelRadiusFront; // 前轮距离中心距离
				var wheelHalfTrackFront = w * 0.5;   // 前轮间距
				var wheelAxisHeightFront = -wheelRadiusFront * 0.5;  // 前轮高度

        var wheelRadiusBack = h * 0.125;
        var wheelWidthBack = 1.5;
        var wheelAxisPositionBack = -l * 0.5 + wheelRadiusBack;
				var wheelHalfTrackBack = w * 0.5;
				var wheelAxisHeightBack = -wheelRadiusBack * 0.5;

				var suspensionStiffness = 20.0;
				var suspensionDamping = 2.3;
				var suspensionCompression = 4.4;
				var suspensionRestLength = 0.6;
				var rollInfluence = 0.2;

				var wheelDirectionCS0 = new Ammo.btVector3(0, -1, 0);
				var wheelAxleCS = new Ammo.btVector3(-1, 0, 0);

        // 底盘 
        // Chassis
        pos.y = (wheelRadiusFront * 2) + (chassisHeight * 0.5);

				var geometry = new Ammo.btBoxShape(new Ammo.btVector3(chassisWidth * .5, chassisHeight * .5, chassisLength * .5));
				var transform = new Ammo.btTransform();
				transform.setIdentity();
				transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
				transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
				var motionState = new Ammo.btDefaultMotionState(transform);
				var localInertia = new Ammo.btVector3(0, 0, 0);
				geometry.calculateLocalInertia(massVehicle, localInertia);
				var body = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(massVehicle, motionState, geometry, localInertia));
				body.setActivationState(DISABLE_DEACTIVATION);
				physicsWorld.addRigidBody(body);
				chassisMesh = createChassisMesh(chassisWidth, chassisHeight, chassisLength);
        
        chassisMesh.position.copy(pos);
        chassisMesh.quaternion.copy(quat);
        chassisMesh.userData.physicsBody = body;

        // Raycast Vehicle
				var engineForce = 0;
				var vehicleSteering = 0;
				var breakingForce = 0;
				var tuning = new Ammo.btVehicleTuning();
				var rayCaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld);
				vehicle = new Ammo.btRaycastVehicle(tuning, body, rayCaster);
				vehicle.setCoordinateSystem(0, 1, 2);
				physicsWorld.addAction(vehicle);
        
        // Wheels

        function createWheelMesh(radius, width) {
          var t = new THREE.CylinderGeometry(radius, radius, width, 24, 1);
          t.rotateZ(Math.PI / 2);
          var mesh = new THREE.Mesh(t, createMaterial());
          mesh.add(new THREE.Mesh(new THREE.BoxGeometry(width + 0.1, radius * 1.75, radius*.25, 1, 1, 1), createMaterial()));
          scene.add(mesh);
          objects.push(mesh);
          return mesh;
        }

        function createChassisMesh(w, l, h) {
          var shape = new THREE.BoxGeometry(w, l, h, 1, 1, 1);
          var mesh = new THREE.Mesh(shape, createMaterial());
          scene.add(mesh);
          objects.push(mesh);
          return mesh;
        }

        function addWheel(isFront, pos, radius, width, index) {

          var wheelInfo = vehicle.addWheel(
              pos,
              wheelDirectionCS0,
              wheelAxleCS,
              suspensionRestLength,
              radius,
              tuning,
              isFront);

          wheelInfo.set_m_suspensionStiffness(suspensionStiffness);
          wheelInfo.set_m_wheelsDampingRelaxation(suspensionDamping);
          wheelInfo.set_m_wheelsDampingCompression(suspensionCompression);
          wheelInfo.set_m_frictionSlip(friction);
          wheelInfo.set_m_rollInfluence(rollInfluence);

          wheelMeshes[index] = createWheelMesh(radius, width);
        }

        wheelMeshes = [];
        addWheel(true, new Ammo.btVector3(wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_LEFT);
				addWheel(true, new Ammo.btVector3(-wheelHalfTrackFront, wheelAxisHeightFront, wheelAxisFrontPosition), wheelRadiusFront, wheelWidthFront, FRONT_RIGHT);
				addWheel(false, new Ammo.btVector3(-wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_LEFT);
				addWheel(false, new Ammo.btVector3(wheelHalfTrackBack, wheelAxisHeightBack, wheelAxisPositionBack), wheelRadiusBack, wheelWidthBack, BACK_RIGHT);

        // 添加车厢
        trunk = new THREE.Group();
        const trunkShape = new Ammo.btCompoundShape(true);
        var transform  = new Ammo.btTransform();
        transform.setIdentity();

        const tk = 0.1;
        const trunkHeight = h - wheelRadiusFront * 2 - chassisHeight;
        // 底部
        function addFrame(x, y, z, pos, quat){
          const threeObject = new THREE.Mesh(new THREE.BoxGeometry(x, y, z), material);
          const shape = new Ammo.btBoxShape( new Ammo.btVector3( x * 0.5, y * 0.5, z * 0.5 ) );
          shape.setMargin( margin );
          transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
          transform.setRotation(new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ));
          threeObject.position.copy(pos);
          threeObject.quaternion.copy(quat);

          trunk.add(threeObject);
          trunkShape.addChildShape(transform, shape);
        }
        pos.set(0, 0, 0);
        quat.set(0, 0, 0, 1);
        addFrame(w, tk, l, pos, quat);
        // front
        pos.set(0, trunkHeight * 0.5, l * 0.5);
        addFrame(w, trunkHeight, tk, pos, quat);
        // back
        pos.set(0, trunkHeight * 0.5, -l * 0.5);
        addFrame(w, trunkHeight, tk, pos, quat);
        // left
        pos.set(w * 0.5, trunkHeight * 0.5, 0);
        addFrame(tk, trunkHeight, l, pos, quat);
        // right
        pos.set(-w * 0.5, trunkHeight * 0.5, 0);
        addFrame(tk, trunkHeight, l, pos, quat);

        pos.copy(chassisMesh.position);
        pos.y += chassisHeight * 0.5;
        quat.setFromAxisAngle(AxisY, -Math.PI / 2);

        createRigidBody(trunk, trunkShape, massVehicle, pos, quat, friction, restitution);
        
        setBodyUserPointer(trunk);

        // 添加约束放置车厢脱落
        var frameInA = new Ammo.btTransform();
        var frameInB = new Ammo.btTransform();

        frameInA.setIdentity();
        frameInB.setIdentity();

        frameInA.setOrigin(new Ammo.btVector3(0, 0, 0));
        frameInB.setOrigin(new Ammo.btVector3(0, 0, 0));

        var lowerSliderLimit = new Ammo.btVector3(0, chassisHeight, 0); 
        var hiSliderLimit = new Ammo.btVector3(0, h, 0); 
        var lowerAngularLimit = new Ammo.btVector3(0,0,0); 
        var hiAngularLimit = new Ammo.btVector3(0,0,0); 

        vehicleConstraint = create6DofConstraint(
          vehicleConstraint,
          body,
          trunk.userData.physicsBody,
          frameInA, frameInB,
          lowerSliderLimit, hiSliderLimit, lowerAngularLimit, hiAngularLimit
        );
      }

      // 拱桥
      function createBridge(){
        pos.copy(chassisMesh.position);
        quat.set(0, 0, 0, 1);
        pos.x -= getParam('vehicle', 'Length');
        pos.y = 0;

        const tk = 0.1;
        const w = getParam('vehicle', 'Width') * 1.5;
        const h = getParam('vehicle', 'Height');
        const bridgeLength = getParam('bridge', 'Length');
        const bridgeHeight = getParam('bridge', 'Height');
        const friction = getParam('bridge', 'Friction');

        const bridgeShape = new THREE.Shape();
        ( function rect( ctx, x, y, width, height ) {

          ctx.moveTo( x, y );
          ctx.lineTo( x + width * 0.5, y );
          ctx.lineTo( x + width * 0.5, y + height );
          ctx.lineTo( x - width * 0.5, y + height );
          ctx.lineTo( x - width * 0.5, y);
          ctx.lineTo( x , y );

        } )( bridgeShape, 0, 0, w, tk );
        
        const extrudePoints = [];
        // const positions = [];
        const step = 1;
        const stepRadian = Math.PI / (bridgeLength / step);
				for ( let i = 0; i <= bridgeLength; i += step ) {
          const x = -i;
          const y = Math.abs(bridgeHeight * Math.sin(stepRadian * i) * 0.25);
          const z = 0;
					extrudePoints.push( new THREE.Vector3( x, y, z) );
          // positions.push(x, y, z);
				}

        // 拉伸线
        // const lineGeometry = new THREE.BufferGeometry();
        // lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
        // lineGeometry.rotateY(-Math.PI/2);
        // const lineMesh = new THREE.Line(lineGeometry, new THREE.LineDashedMaterial( { color: 0xff0000, dashSize: 1, gapSize: 0.5 } ) );
        // lineMesh.computeLineDistances();
        // scene.add(lineMesh);

				const extrudeLine = new THREE.CatmullRomCurve3( extrudePoints );

        const extrudeSettings = { 
          steps: 200,
          bevelEnabled: false, 
          extrudePath: extrudeLine
        };

        const bridgeGeometry = new THREE.ExtrudeGeometry( bridgeShape, extrudeSettings );        
				bridge = new THREE.Mesh( bridgeGeometry, createMaterial() );
        const shape = createConvexHullPhysicsShape(bridgeGeometry.attributes.position.array);
        
        pos.x -= w * 0.5;

        createRigidBody(bridge, shape, 0, pos, quat, friction);

      }

      // 升降梯
      function createLifter(){

        pos.copy(bridge.position);
        quat.set(0, 0, 0, 1);
        pos.x -= getParam('bridge', 'Length');

        const vehicleLength = getParam('vehicle', 'Length');
        const vehicleWidth = getParam('vehicle', 'Width');
        const h = getParam('lifter', 'Height');
        const tk = .1;
        const w = vehicleWidth * 1.5;
        const l = vehicleLength * 1.5;
        const material = createMaterial();

        pos.x -= l * 0.5;
        pos.y = tk * 0.5;
        lifter = createParalellepiped(l, tk, w, 1, pos, quat, material);
        // setBodyUserPointer(lifter);
        lifter.userData.direction = 0;
        lifter.userData.LowerLimit = tk;
        lifter.userData.UpperLimit = h + tk * 0.5;

        const basePos = pos.clone();
        
        pos.set(basePos.x - (l + tk) * 0.5, basePos.y + h * 0.5, basePos.z);
        const lifterBase = createParalellepiped(tk, h, w, 0, pos, quat, material);

        pos.set(basePos.x, basePos.y + h * 0.5, basePos.z - (w + tk) * 0.5);
        const lifterWall1 = createParalellepiped(l, h, tk, 0, pos, quat, material);
        pos.set(basePos.x, basePos.y + h * 0.5, basePos.z + (w + tk) * 0.5);
        const lifterWall2 = createParalellepiped(l, h, tk, 0, pos, quat, material);

        // 取消重力对升降梯的影响
        lifter.userData.physicsBody.setGravity(0, 0, 0);
        // 添加约束防止升降梯旋转
        var frameInA = new Ammo.btTransform();
        var frameInB = new Ammo.btTransform();

        frameInA.setIdentity();
        frameInB.setIdentity();
        frameInA.setOrigin(new Ammo.btVector3((l+tk) * 0.5, 0.0, 0.0));
        frameInB.setOrigin(new Ammo.btVector3(0.0, 0.0, 0.0));

        frameInA.getBasis().setEulerZYX(0, 0, Math.PI/2);
        frameInB.getBasis().setEulerZYX(0, 0, Math.PI/2);

        liftConstraint = createSliderConstraint(
          liftConstraint,
          lifterBase.userData.physicsBody, lifter.userData.physicsBody,
          frameInA, frameInB,
          -h* 0.5 - tk, h * 0.5
        );
      }

      // 推土机:角速度
      function createBulldozer(){
        pos.copy(lifter.position);
        quat.set(0, 0, 0, 1);

        const vehicleLength = getParam('vehicle', 'Length');
        const vehicleWidth = getParam('vehicle', 'Width');
        const vehicleHeight = getParam('vehicle', 'Height');
        const lifterHeight = getParam('lifter', 'Height');
        const tk = getParam('ramp', 'Height');
        const r = getParam('bulldozer', 'Radius');
        const w = vehicleWidth * 1.5;
        const l = vehicleLength * 1.5;
        const material = createMaterial();

        pos.y = lifterHeight;
        const basePos = pos.clone();
        // 护栏
        pos.set(basePos.x + (l + tk) * 0.5 , basePos.y + vehicleHeight * 0.5, basePos.z + w * 0.5);
        const guard1 = createParalellepiped(tk, vehicleHeight, w * 2, 0, pos, quat, material);
        pos.set(basePos.x -  (l + tk) * 0.5 , basePos.y + vehicleHeight * 0.5, basePos.z + w * 0.5);
        const guard2 = createParalellepiped(tk, vehicleHeight, w * 2, 0, pos, quat, material);
        pos.set(basePos.x , basePos.y - (tk + w * Math.sin(Math.PI * 0.1)) * 0.5, basePos.z + w);
        quat.setFromAxisAngle(AxisX, Math.PI / 10);
        const guard3 = createParalellepiped(l, tk, w , 0, pos, quat, material);

        pos.set(basePos.x, basePos.y + r * 0.5, basePos.z - w * 0.5);
        bulldozer = createParalellepiped(l, r, tk, 10, pos, quat, material);
        pos.set(basePos.x, basePos.y + r  + tk * 0.5, basePos.z - w * 0.5);
        quat.setFromAxisAngle(AxisZ, Math.PI/2);
        const bulldozerBase = createCylindered(tk, l, 0, pos, quat, material);

        const pivotA = new Ammo.btVector3( 0, 0, 0 );
				const pivotB = new Ammo.btVector3( 0, r * 0.5, 0 );
				const axisA = new Ammo.btVector3( 0, 1, 0 );
        const axisB = new Ammo.btVector3( -1, 0, 0 );
        bulldozerHinge = createHingeConstraint(
          bulldozerHinge, 
          bulldozerBase.userData.physicsBody, bulldozer.userData.physicsBody,
          pivotA, pivotB, axisA, axisB
        );

        bulldozerHinge.enableAngularMotor(false, 0, 100);
      }

      // 气垫: 软体
      function createCushion(){
        pos.copy(lifter.position);
        quat.set(0, 0, 0, 1);
        pos.z += getParam('vehicle', 'Width') * 2.5;

        const vehicleLength = getParam('vehicle', 'Length');
        const tk = 1;
        const w = getParam('cushion', 'Width');
        const h = getParam('cushion', 'Height');
        const l = getParam('cushion', 'Length');
        const mass = getParam('cushion', 'Mass');
        const pressure = getParam('cushion', 'Pressure');

        const cushionGeometry = new THREE.BoxGeometry(w, h, l, 10, 10, 20);
        pos.z += w * 0.5;
        pos.y = h * 0.5;
        const basePos = pos.clone();
        cushionGeometry.translate(pos.x, pos.y, pos.z);

        pos.set(basePos.x - w * 0.5 - tk, h, basePos.z);
        const wall3 = createParalellepiped(tk, h * 2, l + tk, 0, pos, quat, createMaterial());
        pos.set(basePos.x, h, basePos.z + l * 0.5 + tk);        
        const wall1 = createParalellepiped(w, h * 2, tk, 0, pos, quat, createMaterial());
        pos.set(basePos.x, h * 0.5, basePos.z - l * 0.5 - tk);     
        const wall2 = createParalellepiped(w, h, tk, 0, pos, quat, createMaterial());

        cushion = createSoftVolume( cushionGeometry, mass, pressure );
        setBodyUserPointer(cushion);        
      }

      // 多米诺：易碎品
      function createDominos(){
        dominos = [];

        pos.copy(lifter.position);
        quat.set(0, 0, 0, 1);
        pos.z += getParam('vehicle', 'Width') * 2.5 + getParam('cushion', 'Length') * 0.5;

        const count = params.dominoCount;
        const mass = params.dominoMass;
        const x = params.rampHeight;
        const y = params.dominoHeight;
        const z = params.startPlatformWidth;
        const r = getParam('ground', 'Size') * 0.6;
        
        pos.x += getParam('cushion', 'Width') * 0.5 + y * 0.35;
        pos.y  = y / 2;
        // pos.z -= r;
        const center = new THREE.Vector3();
        center.copy(pos);

        // const centerMesh = createSphered(1, 0, pos, quat, createMaterial());
        const setpRy = Math.PI * 0.5 / count;

        function createDominoObject( mass, halfExtents, pos, quat, material ) {

          const object = new THREE.Mesh( new THREE.BoxGeometry( halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2 ), material );
          object.position.copy( pos );
          object.quaternion.copy( quat );
          convexBreaker.prepareBreakableObject( object, mass, new THREE.Vector3(), new THREE.Vector3(), true );
          
          createDebrisFromBreakableObject( object );
          // scene.add( object )
 
          return object;
        }
        for(var i = 0; i <= count; i++){

          const h = y;
          pos.x = center.x + i * h * 0.6;
          // pos.x = center.x + r * Math.sin(setpRy * i);
          pos.y = h * 0.5;
          // pos.z = center.z + r * Math.cos(setpRy * i);
      

          // quat.setFromAxisAngle(AxisY, setpRy * i);
          const halfExtents = new THREE.Vector3( x * 0.5, h * 0.5, z * 0.5 );
          const domino = createDominoObject( mass, halfExtents, pos, quat, createMaterial());
          // domino.castShadow = true;
          // domino.receiveShadown = true;

          // setBodyUserPointer(domino);

          dominos.push(domino);
        }

      }


      // 圆环：收集小球
      function createHoop(){
        const r = getParam('slider', 'Size');

        pos.copy(seesaw.position);
        pos.x += getParam('startPlatform', 'Width') + 4;
        pos.y = r;
        pos.z -= getParam('seesaw', 'Length') * 0.5;

        createTorus(r*3, r, 0, pos, quat);
      }

      // 测试
      var count = 0;
      function debugOnly(){
        count = (count + 1) % 100;
        if(count != 0) return;

        const pos = getPosition(params.startPlatformPosition);
        const quat = new THREE.Quaternion();
        pos.y += params.startPlatformHeight / 2 + 0.5;

        const debugBall = createSphered(
          1,
          1, pos, quat, createMaterial()
        );
      }

      function createObjects() {
        // const pos = new THREE.Vector3();
        // const quat = new THREE.Quaternion();

        // Ground
        createGround();

        createPlatform();
        createPoolCue();

        followedObject = createStartBall();
        createRamp();
        createHHitBridge();
        createHHitBall();
        createPendulum();
        createSlider();
        createSeesaw();
        createBalance();
        createHopper();
        createVehicle();
        createBridge();
        createLifter();
        createBulldozer();
        createCushion();
        
        createDominos();

        // // 循环系统
        // createHoop();
      }

      function initInput() {

        window.addEventListener( 'keydown', function ( event ) {


        } );

        window.addEventListener( 'keyup', function () {

        } );

      }

      function setupGUI(){
        
        gui = new GUI();

        gui.add(params, 'clearParams').name('清除配置');
        gui.add(params, 'saveParams').name('保存配置');
        // 场景
        const guiScene = gui.addFolder('场景');
        
        guiScene.addColor(params, 'sceneBackGroundColor').name('背景色').onChange((val)=>{
          scene.background.set(val);
        });
        guiScene.addColor(params, 'sceneLight').name('环境光').onChange((val)=>{
          ambientLight.color.set(val);
        });
        guiScene.add(params, 'cameraFov', 1, 360, 1).name('相机视角').onChange((val)=>{
          camera.fov = val;
          camera.updateProjectionMatrix();
        });
        guiScene.add(params, 'cameraFar', 10, 9999, 1).name('相机远景').onChange((val)=>{
          camera.far = val;
          camera.updateProjectionMatrix();
        });
        guiScene.add(params, 'cameraNear', 0.01, 10, 0.01).name('相机近景').onChange((val)=>{
          camera.near = val;
          camera.updateProjectionMatrix();
        });

        guiScene.add(params, 'groundSize', 10, 888, 10).name('场地大小').onChange((val)=>{
          createGround();
        });

        guiCameraPos = guiScene.add(params, 'cameraPosition').name('位置');
        guiCameraEuler = guiScene.add(params, 'cameraEuler').name('旋转');
        guiCameraLookAt = guiScene.add(params, 'cameraLookAt').name('目标');

        // 仿真
        const guiSimulation = gui.addFolder('仿真');
        const guiSimStart = guiSimulation.add(params, 'simStart').name('开始').onChange((val)=>{
          if(val){
            guiSimStart.name('开始');
            guiSimReset.setValue(false);
          }
          else{
            guiSimStart.name('停止');
          }
        });
        const guiSimPause = guiSimulation.add(params, 'simPause').name('暂停').onChange((val)=>{
          if(!val){
            guiSimPause.name('暂停');
          }
          else{
            guiSimPause.name('继续');
          }
        });
        const guiSimReset = guiSimulation.add(params, 'simReset').name('重置').onChange((val)=>{
          if(val){
            reset();
            guiSimStart.setValue(true);
            guiSimPause.setValue(false);
          }
        });
        guiSimulation.add(params, 'simSpeed').name('仿真速度').options(params.simSpeeds);

        const guiObjects = gui.addFolder('物体');
        addObjectParmas(guiObjects, '起始平台', 'startPlatform', reset);
        addObjectParmas(guiObjects, '球杆', 'poolCue', reset);
        addObjectParmas(guiObjects, '起始小球', 'startBall', reset);
        addObjectParmas(guiObjects, '斜坡', 'ramp', reset);
        addObjectParmas(guiObjects, '水平桥', 'hHitBridge', reset);
        addObjectParmas(guiObjects, '水平球', 'hHitBall', reset);
        addObjectParmas(guiObjects, '多米诺', 'domino', reset);
        addObjectParmas(guiObjects, '钟摆', 'pendulum', reset);
        addObjectParmas(guiObjects, '滑板', 'slider', reset);
        addObjectParmas(guiObjects, '跷跷板', 'seesaw', reset);
        addObjectParmas(guiObjects, '天平', 'balance', reset);
        addObjectParmas(guiObjects, '漏斗', 'hopper', reset);
        addObjectParmas(guiObjects, '小车', 'vehicle', reset);
        addObjectParmas(guiObjects, '拱桥', 'bridge', reset);
        addObjectParmas(guiObjects, '升降梯', 'lifter', reset);
        addObjectParmas(guiObjects, '推土机', 'bulldozer', reset);
        addObjectParmas(guiObjects, '气垫', 'cushion', reset);

        gui.close();
      }

      function addObjectParmas(gui, objName, objKey, callback, keyList=objectParams){
        const startPt = gui.addFolder(objName);
        for(var i = 0, il = keyList.length; i < il; i++){
          const paramsKey = objKey+keyList[i];
          if(params[paramsKey]){
            startPt.add(params, paramsKey).name(keyList[i]).onChange(callback);
          }
        }
      }

      function onWindowResize() {

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        followedCamera.aspect = window.innerWidth / window.innerHeight;
        followedCamera.updateProjectionMatrix();

        renderer.setSize( window.innerWidth, window.innerHeight );

      }

      function createMaterial( color ) {

        return new THREE.MeshPhongMaterial( { 
          color: color?color:createRandomColor(), 
          transparent: false, 
          opacity: 0.5
        } );

      }

      function createRandomColor() {

        return Math.floor( Math.random() * ( 1 << 24 ) );

      }
      // 空心圆环
      function createTorus(radius, tube, mass, pos, quat ){
        const tPos = pos.clone();
        const tQuat = quat.clone();
        const torusPart = 4;
        // 圆环
        const torusGroup = new THREE.Group();
        const torusRadius = radius;
        const torusTube = tube;
        var compound = new Ammo.btCompoundShape(true);
        var transform  = new Ammo.btTransform();
        transform.setIdentity();

        for(var i = 0; i < torusPart; i ++){
          var torusGeometry = new THREE.TorusGeometry( torusRadius, torusTube, 48, 48, Math.PI * 2 / torusPart);
          torusGeometry.rotateZ(i * ((Math.PI * 2) / torusPart));
          const torus = new THREE.Mesh(torusGeometry, createMaterial());
          pos.set(0,0,0);
          quat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
          torus.position.copy(pos);
          torus.quaternion.copy(quat);
          torusGroup.add(torus);

          const torusShape = createConvexHullPhysicsShape(torusGeometry.attributes.position.array);
          transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
          transform.setRotation(new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ));
          compound.addChildShape(transform, torusShape);
        }
        
        const torusBody = createRigidBody(torusGroup, compound, mass, tPos, tQuat);
      
        torusBody.setFriction(0.5);
        // torusBody.setAngularVelocity(new Ammo.btVector3(0, -Math.PI, 0));
        return torusGroup;
      }

      function addChildShape(object, compound, type, x, y, z, pos, quat, material){
        var transform  = new Ammo.btTransform();
        transform.setIdentity();

        switch(type){
          case 'box':
            const threeObject = new THREE.Mesh(new THREE.BoxGeometry(x, y, z), material);
            const shape = new Ammo.btBoxShape( new Ammo.btVector3( x * 0.5, y * 0.5, z * 0.5 ) );
            shape.setMargin( margin );
            transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
            transform.setRotation(new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ));
            threeObject.position.copy(pos);
            threeObject.quaternion.copy(quat);

            object.add(threeObject);
            compound.addChildShape(transform, shape);
          break;
          default:break;
        }
      }

      function createCylindered ( radius, height, mass, pos, quat, material ){
        var threeObject = new THREE.Mesh( new THREE.CylinderGeometry( radius , radius, height, 20, 1), material );
        var shape = new Ammo.btCylinderShape( new Ammo.btVector3( radius, height * 0.5, radius ) );
        shape.setMargin( margin );

        createRigidBody( threeObject, shape, mass, pos, quat );

        return threeObject;
      }

      function createConvexObject( vertices, mass, pos, quat, material, friction, restitution ){
        const geometry = new ConvexGeometry(vertices);
        var threeObject = new THREE.Mesh( geometry, material );
        var shape = createConvexHullPhysicsShape(geometry.attributes.position.array);
        createRigidBody( threeObject, shape, mass, pos, quat, friction, restitution );
        return threeObject;
      }

      function createParalellepiped( sx, sy, sz, mass, pos, quat, material ) {

        const threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), material );
        const shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
        shape.setMargin( margin );

        createRigidBody( threeObject, shape, mass, pos, quat );

        return threeObject;

      }

      function createConvexHullPhysicsShape( coords ) {
        const shape = new Ammo.btConvexHullShape();
        var tempBtVec3_1 = new Ammo.btVector3( 0, 0, 0 );
        for ( let i = 0, il = coords.length; i < il; i += 3 ) {

          tempBtVec3_1.setValue( coords[ i ], coords[ i + 1 ], coords[ i + 2 ] );
          const lastOne = ( i >= ( il - 3 ) );
          shape.addPoint( tempBtVec3_1, lastOne );

        }

        shape.setMargin(margin);
        return shape;

      }

      function createSphered( r, mass, pos, quat, material , friction, restitution) {

        const threeObject = new THREE.Mesh( new THREE.SphereGeometry( r, 24, 24 ), material );
        const shape = new Ammo.btSphereShape( r );
        shape.setMargin( margin );

        createRigidBody( threeObject, shape, mass, pos, quat, friction, restitution);

        return threeObject;

      }
      
      function createRigidBody( threeObject, physicsShape, 
        mass, pos, quat , 
        friction, restitution, inertia,
        vel, angVel) {

        if ( pos ) {

          threeObject.position.copy( pos );

        } else {

          pos = threeObject.position;

        }

        if ( quat ) {

          threeObject.quaternion.copy( quat );

        } else {

          quat = threeObject.quaternion;

        }

        const transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
        transform.setRotation( new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ) );
        const motionState = new Ammo.btDefaultMotionState( transform );

        var localInertia = new Ammo.btVector3( 0, 0, 0 );
        
        if(inertia){
          localInertia = inertia;
        }

        physicsShape.calculateLocalInertia( mass, localInertia );

        const rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, physicsShape, localInertia );
        const body = new Ammo.btRigidBody( rbInfo );

        threeObject.userData.physicsBody = body;

        scene.add( threeObject );
        
        threeObject.castShadow = true;
        threeObject.receiveShadow = true;

        objects.push( threeObject );

        if ( vel ) {

          body.setLinearVelocity( new Ammo.btVector3( vel.x, vel.y, vel.z ) );

        }

        if ( angVel ) {

          body.setAngularVelocity( new Ammo.btVector3( angVel.x, angVel.y, angVel.z ) );

        }

        threeObject.userData.collided = false;

        if ( mass > 0 ) {

          rigidBodies.push( threeObject );

          // Disable deactivation
          body.setActivationState( 4 );

        }

        if(friction){
          body.setFriction(friction);
        }
        // else{
        //   body.setFriction(0.5);
        // }

        if(restitution){
          body.setRestitution(restitution);
        }
        // else{
        //   body.setRestitution(0.5);
        // }

        physicsWorld.addRigidBody( body );
        
        return body;

      }

      function createHingeConstraint(hinge, bodyA, bodyB, pivotA, pivotB, axisA, axisB){
        if(hinge){
          physicsWorld.removeConstraint(hinge);
        }
				hinge = new Ammo.btHingeConstraint( 
          bodyA, bodyB, 
          pivotA, pivotB, axisA, axisB, true );
				physicsWorld.addConstraint( hinge, true );
        // hinge.enableAngularMotor( true, -0.8 * 1, 50 );

        return hinge;
      }

      function createSliderConstraint(constraint, bodyA, bodyB, frameInA, frameInB, lowerLimit, upperLimit){
        if(constraint){
          physicsWorld.removeConstraint(constraint);
        }
        constraint = new Ammo.btSliderConstraint( bodyA, bodyB, frameInA, frameInB, false );
        
        physicsWorld.addConstraint( constraint, true );

        constraint.setLowerLinLimit(lowerLimit);
        constraint.setUpperLinLimit(upperLimit);
        constraint.setLowerAngLimit(0);
        constraint.setUpperAngLimit(0);

        return constraint;
      }

      function create6DofConstraint(
        constraint, bodyA, bodyB, frameInA, frameInB, 
        lowerSliderLimit, hiSliderLimit, lowerAngularLimit, hiAngularLimit
      ){

        if(constraint){
          physicsWorld.removeConstraint(constraint);
        }

        constraint = new Ammo.btGeneric6DofSpringConstraint  (
          bodyA,
          bodyB,
          frameInA,
          frameInB,
          true
        );

        if(lowerSliderLimit) constraint.setLinearLowerLimit(lowerSliderLimit);
        if(hiSliderLimit) constraint.setLinearUpperLimit(hiSliderLimit);
        if(lowerAngularLimit) constraint.setAngularLowerLimit(lowerAngularLimit);
        if(hiAngularLimit) constraint.setAngularUpperLimit(hiAngularLimit);

        physicsWorld.addConstraint(constraint)

        return constraint;
      }

      function createDebrisFromBreakableObject( object ) {
        
        object.castShadow = true;
        object.receiveShadow = true;
        
        const shape = createConvexHullPhysicsShape( object.geometry.attributes.position.array );
        // const shape = getMeshShape( object, null, false );
        shape.setMargin( margin );

        // const shape = new Ammo.btEmptyShape();
        const body = createRigidBody( object, shape, 
          object.userData.mass, null, null,
          0.5, 1, undefined,
          object.userData.velocity, object.userData.angularVelocity );

        scene.add( object );

        // Set pointer back to the three object only in the debris objects
        const btVecUserData = new Ammo.btVector3( 0, 0, 0 );
        btVecUserData.threeObject = object;
        body.setUserPointer( btVecUserData );

        return object;
      }

      function processGeometry( bufGeometry ) {

        // Ony consider the position values when merging the vertices
        const posOnlyBufGeometry = new THREE.BufferGeometry();
        posOnlyBufGeometry.setAttribute( 'position', bufGeometry.getAttribute( 'position' ) );
        posOnlyBufGeometry.setIndex( bufGeometry.getIndex() );

        // Merge the vertices so the triangle soup is converted to indexed triangles
        const indexedBufferGeom = BufferGeometryUtils.mergeVertices( posOnlyBufGeometry );

        // Create index arrays mapping the indexed vertices to bufGeometry vertices
        mapIndices( bufGeometry, indexedBufferGeom );

      }

      function isEqual( x1, y1, z1, x2, y2, z2 ) {

        const delta = 0.000001;
        return Math.abs( x2 - x1 ) < delta &&
            Math.abs( y2 - y1 ) < delta &&
            Math.abs( z2 - z1 ) < delta;

      }

      function mapIndices( bufGeometry, indexedBufferGeom ) {

        // Creates ammoVertices, ammoIndices and ammoIndexAssociation in bufGeometry

        const vertices = bufGeometry.attributes.position.array;
        const idxVertices = indexedBufferGeom.attributes.position.array;
        const indices = indexedBufferGeom.index.array;

        const numIdxVertices = idxVertices.length / 3;
        const numVertices = vertices.length / 3;

        bufGeometry.ammoVertices = idxVertices;
        bufGeometry.ammoIndices = indices;
        bufGeometry.ammoIndexAssociation = [];

        for ( let i = 0; i < numIdxVertices; i ++ ) {

          const association = [];
          bufGeometry.ammoIndexAssociation.push( association );

          const i3 = i * 3;

          for ( let j = 0; j < numVertices; j ++ ) {

            const j3 = j * 3;
            if ( isEqual( idxVertices[ i3 ], idxVertices[ i3 + 1 ], idxVertices[ i3 + 2 ],
              vertices[ j3 ], vertices[ j3 + 1 ], vertices[ j3 + 2 ] ) ) {

              association.push( j3 );

            }

          }

        }

      }

      function createSoftVolume( bufferGeom, mass, pressure ) {

        processGeometry( bufferGeom );

        const volume = new THREE.Mesh( bufferGeom, createMaterial() );
        volume.castShadow = true;
        volume.receiveShadow = true;
        volume.frustumCulled = false;
        scene.add( volume );
        objects.push(volume);
        // Volume physic object

        const volumeSoftBody = softBodyHelpers.CreateFromTriMesh(
          physicsWorld.getWorldInfo(),
          bufferGeom.ammoVertices,
          bufferGeom.ammoIndices,
          bufferGeom.ammoIndices.length / 3,
          true );

        const sbConfig = volumeSoftBody.get_m_cfg();
        sbConfig.set_viterations( 40 );
        sbConfig.set_piterations( 40 );

        // Soft-soft and soft-rigid collisions
        sbConfig.set_collisions( 0x11 );

        // Friction
        sbConfig.set_kDF( 0.1 );
        // Damping
        sbConfig.set_kDP( 0.01 );
        // Pressure
        sbConfig.set_kPR( pressure );
        // Stiffness
        volumeSoftBody.get_m_materials().at( 0 ).set_m_kLST( 0.9 );
        volumeSoftBody.get_m_materials().at( 0 ).set_m_kAST( 0.9 );

        volumeSoftBody.setTotalMass( mass, false );
        Ammo.castObject( volumeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin );
        physicsWorld.addSoftBody( volumeSoftBody, 1, - 1 );
        volume.userData.physicsBody = volumeSoftBody;
        // Disable deactivation
        volumeSoftBody.setActivationState( 4 );

        softBodies.push( volume );

        return volume;
      }

      function createRope(rope, bodyA, bodyB, ropeLength, ballRadius){
        disposeObject(rope);

        const ropeNumSegments = 10;
			  // const ropeLength = 4;
			  const ropeMass = 1;
			  const ropePos = bodyB.position.clone();
        
			  ropePos.y += ballRadius;

        const segmentLength = ropeLength / ropeNumSegments;
        const ropeGeometry = new THREE.BufferGeometry();
        const ropeMaterial = new THREE.LineBasicMaterial( { color: 0x000000 } );
        const ropePositions = [];
        const ropeIndices = [];

        for ( let i = 0; i < ropeNumSegments + 1; i ++ ) {

          ropePositions.push( ropePos.x, ropePos.y + i * segmentLength, ropePos.z );

        }

        for ( let i = 0; i < ropeNumSegments; i ++ ) {

          ropeIndices.push( i, i + 1 );

        }

        ropeGeometry.setIndex( new THREE.BufferAttribute( new Uint16Array( ropeIndices ), 1 ) );
        ropeGeometry.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array( ropePositions ), 3 ) );
        ropeGeometry.computeBoundingSphere();
        rope = new THREE.LineSegments( ropeGeometry, ropeMaterial );
        rope.castShadow = true;
        rope.receiveShadow = true;
        scene.add( rope );

        // Rope physic object
        const softBodyHelpers = new Ammo.btSoftBodyHelpers();
        const ropeStart = new Ammo.btVector3( ropePos.x, ropePos.y, ropePos.z );
        const ropeEnd = new Ammo.btVector3( ropePos.x, ropePos.y + ropeLength, ropePos.z );
        const ropeSoftBody = softBodyHelpers.CreateRope( physicsWorld.getWorldInfo(), ropeStart, ropeEnd, ropeNumSegments - 1, 0 );
        const sbConfig = ropeSoftBody.get_m_cfg();
        sbConfig.set_viterations( 10 );
        sbConfig.set_piterations( 10 );
        ropeSoftBody.setTotalMass( ropeMass, false );
        Ammo.castObject( ropeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin * 3 );
        physicsWorld.addSoftBody( ropeSoftBody, 1, - 1 );
        rope.userData.physicsBody = ropeSoftBody;
        // Disable deactivation
        ropeSoftBody.setActivationState( 4 );

        // Glue the rope extremes to the ball and the arm
        const influence = 1;
        ropeSoftBody.appendAnchor( 0, bodyB.userData.physicsBody, true, influence );
        ropeSoftBody.appendAnchor( ropeNumSegments, bodyA.userData.physicsBody, true, influence );

        return rope;
      }

      function animate() {

        requestAnimationFrame( animate );

        render();
        stats.update();

      }

      function render() {

        const deltaTime = clock.getDelta() / params.simSpeed;
        updatePhysics( deltaTime )
        
        renderer.setViewport( 0, 0,  window.innerWidth, window.innerHeight );
        renderer.render( scene, camera ); 

        // 追踪镜头
        renderer.setScissorTest(true);
        renderer.setScissor(params.panaromicOffset, params.panaromicOffset, params.followedWidth * window.innerWidth, params.followedHeight * window.innerHeight);
        renderer.setViewport( params.panaromicOffset, params.panaromicOffset, params.followedWidth  * window.innerWidth, params.followedHeight  * window.innerHeight);

      
        if(followedObject){
          const pos = followedObject.position;
          followedCamera.position.set( pos.x + 5, pos.y + 5, pos.z + 5);
          followedCamera.lookAt(pos.x, pos.y, pos.z);
        }
        else{
          followedCamera.position.copy(camera.position);
          followedCamera.quaternion.copy(camera.quaternion);
        }
        
        renderer.render( scene, followedCamera );

        renderer.setScissorTest(false);

        // 更新镜头位置信息
        const euler = camera.rotation;
        guiCameraPos.setValue(`${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`);
        guiCameraEuler.setValue(`${(euler.x*180/Math.PI).toFixed(2)},${(euler.y*180/Math.PI).toFixed(2)},${(euler.z*180/Math.PI).toFixed(2)}`);
        guiCameraLookAt.setValue(`${controls.target.x.toFixed(2)},${controls.target.y.toFixed(2)},${controls.target.z.toFixed(2)}`);
      }

      function updatePhysics( deltaTime ) {

        // Step world
        physicsWorld.stepSimulation( deltaTime );

        // Update soft bodies
        updateSoftBodies();
        // Update rigid bodies
        for ( let i = 0, il = rigidBodies.length; i < il; i ++ ) {

          const objThree = rigidBodies[ i ];
          const objPhys = objThree.userData.physicsBody;
          const ms = objPhys.getMotionState();
          if ( ms ) {

            ms.getWorldTransform( transformAux1 );
            const p = transformAux1.getOrigin();
            const q = transformAux1.getRotation();
            objThree.position.set( p.x(), p.y(), p.z() );
            objThree.quaternion.set( q.x(), q.y(), q.z(), q.w() );

            // 清除超出边界的物体
            if(p.y() < -1){
              objectsToRemove[ numObjectsToRemove ++ ] = objThree;
              // objThree = undefined;
              if(objThree === followedObject){
                followedObject = undefined;
              }
              continue;
            }
          }
        }
        
        poolCueWork();
        updateFollowedObject(); 
        updateRope(pendulumRope);
        updateVehicle();
        lifterWork();

        for ( let i = 0; i < numObjectsToRemove; i ++ ) {

          disposeObject( objectsToRemove[ i ] );

        }

        numObjectsToRemove = 0;
      }

      function updateSoftBodies(){
        // Update soft volumes
				for ( let i = 0, il = softBodies.length; i < il; i ++ ) {

          const volume = softBodies[ i ];
          const geometry = volume.geometry;
          const softBody = volume.userData.physicsBody;
          const volumePositions = geometry.attributes.position.array;
          const volumeNormals = geometry.attributes.normal.array;
          const association = geometry.ammoIndexAssociation;
          const numVerts = association.length;
          const nodes = softBody.get_m_nodes();
          for ( let j = 0; j < numVerts; j ++ ) {

            const node = nodes.at( j );
            const nodePos = node.get_m_x();
            const x = nodePos.x();
            const y = nodePos.y();
            const z = nodePos.z();
            const nodeNormal = node.get_m_n();
            const nx = nodeNormal.x();
            const ny = nodeNormal.y();
            const nz = nodeNormal.z();

            const assocVertex = association[ j ];

            for ( let k = 0, kl = assocVertex.length; k < kl; k ++ ) {

              let indexVertex = assocVertex[ k ];
              volumePositions[ indexVertex ] = x;
              volumeNormals[ indexVertex ] = nx;
              indexVertex ++;
              volumePositions[ indexVertex ] = y;
              volumeNormals[ indexVertex ] = ny;
              indexVertex ++;
              volumePositions[ indexVertex ] = z;
              volumeNormals[ indexVertex ] = nz;

            }

          }

          geometry.attributes.position.needsUpdate = true;
          geometry.attributes.normal.needsUpdate = true;

        }
      }

      function updateVehicle(){
        var speed = vehicle.getCurrentSpeedKmHour();
        // console.log(speed)
        var breakingForce = 0;
				var engineForce = 0;

        var vehicleSteering = 0;  // 转向参数

        if(vehicleCanMove && speed <= maxSpeed){
          engineForce = maxEngineForce;
        }
        else{
          breakingForce = maxBreakingForce;
        }

        vehicle.applyEngineForce(engineForce, BACK_LEFT);
        vehicle.applyEngineForce(engineForce, BACK_RIGHT);

        vehicle.setBrake(breakingForce / 2, FRONT_LEFT);
        vehicle.setBrake(breakingForce / 2, FRONT_RIGHT);
        vehicle.setBrake(breakingForce, BACK_LEFT);
        vehicle.setBrake(breakingForce, BACK_RIGHT);

        vehicle.setSteeringValue(vehicleSteering, FRONT_LEFT);
        vehicle.setSteeringValue(vehicleSteering, FRONT_RIGHT);

        var tm, p, q, i;
        var n = vehicle.getNumWheels();
        for (i = 0; i < n; i++) {
          vehicle.updateWheelTransform(i, true);
          tm = vehicle.getWheelTransformWS(i);
          p = tm.getOrigin();
          q = tm.getRotation();
          wheelMeshes[i].position.set(p.x(), p.y(), p.z());
          wheelMeshes[i].quaternion.set(q.x(), q.y(), q.z(), q.w());
        }

        tm = vehicle.getChassisWorldTransform();
        p = tm.getOrigin();
        q = tm.getRotation();
        chassisMesh.position.set(p.x(), p.y(), p.z());
        chassisMesh.quaternion.set(q.x(), q.y(), q.z(), q.w());
      }

      function updateFollowedObject(){
        for ( let i = 0, il = dispatcher.getNumManifolds(); i < il; i ++ ) {

          const contactManifold = dispatcher.getManifoldByIndexInternal( i );
          const rb0 = Ammo.castObject( contactManifold.getBody0(), Ammo.btRigidBody );
          const rb1 = Ammo.castObject( contactManifold.getBody1(), Ammo.btRigidBody );
          
          const threeObject0 = Ammo.castObject( rb0.getUserPointer(), Ammo.btVector3 ).threeObject;
          const threeObject1 = Ammo.castObject( rb1.getUserPointer(), Ammo.btVector3 ).threeObject;

          if ( ! threeObject0 || ! threeObject1 ) {

            continue;

          }

          updateDominos(threeObject0, threeObject1, contactManifold);

          

          if(followedObject === threeObject0 && preFollowedObject !== threeObject1){
            preFollowedObject = threeObject0;
            followedObject = threeObject1;
            // console.log(followedObject)
          }
          else if(followedObject === threeObject1 && preFollowedObject !== threeObject0){
            preFollowedObject = threeObject1;
            followedObject = threeObject0;
            // console.log(followedObject)
          }
          
          if(followedObject === trunk){
            vehicleCanMove = true;
          }
        }
      }

      function updateRope(rope){
        // Update rope
        const softBody = rope.userData.physicsBody;
        const ropePositions = rope.geometry.attributes.position.array;
        const numVerts = ropePositions.length / 3;
        const nodes = softBody.get_m_nodes();
        let indexFloat = 0;

        for ( let i = 0; i < numVerts; i ++ ) {

          const node = nodes.at( i );
          const nodePos = node.get_m_x();
          ropePositions[ indexFloat ++ ] = nodePos.x();
          ropePositions[ indexFloat ++ ] = nodePos.y();
          ropePositions[ indexFloat ++ ] = nodePos.z();

        }

        rope.geometry.attributes.position.needsUpdate = true;
      }

      function castPhysicsRay( origin, dest, intersectionPoint, intersectionNormal ) {
        // Returns true if ray hit, and returns intersection data on the last two vector parameters
        // TODO Mask and group filters can be added to the test (rayCallBack.m_collisionFilterGroup and m_collisionFilterMask)
        
        // Reset closestRayResultCallback to reuse it
        var rayCallBack = Ammo.castObject( closestRayResultCallback, Ammo.RayResultCallback );
        rayCallBack.set_m_closestHitFraction( 1 );
        rayCallBack.set_m_collisionObject( null );

        // Set closestRayResultCallback origin and dest
        tempVRayOrigin.setValue( origin.x, origin.y, origin.z );
        tempVRayDest.setValue( dest.x, dest.y, dest.z );
        closestRayResultCallback.get_m_rayFromWorld().setValue( origin.x, origin.y, origin.z );
        closestRayResultCallback.get_m_rayToWorld().setValue( dest.x, dest.y, dest.z );

        // Perform ray test
        physicsWorld.rayTest( tempVRayOrigin, tempVRayDest, closestRayResultCallback );

        if ( closestRayResultCallback.hasHit() ) {

          if ( intersectionPoint ) {
            var point = closestRayResultCallback.get_m_hitPointWorld();
            intersectionPoint.set( point.x(), point.y(), point.z() );
          }

          if ( intersectionNormal ) {
            var normal = closestRayResultCallback.get_m_hitNormalWorld();
            intersectionNormal.set( normal.x(), normal.y(), normal.z() );
          }

          return true;

        }
        else {
          poolCueRay.material.color.setRGB(0, 1,  0);
          return false;

        }

      }

      function poolCueWork(){
        const distance = poolCue.position.x - poolCueBase.position.x;
        const dest = new THREE.Vector3();
        dest.copy(poolCue.position);
        dest.x += params.startPlatformLength;

        switch(poolCue.userData.direction){
          case 0:
            if(castPhysicsRay(poolCue.position, dest)){
              poolCue.userData.direction = 1;
              poolCueRay.material.color.setRGB(1, 0,  0);
            }
            else{
              poolCueRay.material.color.setRGB(0, 1,  0);
            }
            break;
          case -1:
            if(distance <= poolCue.userData.LowerLimit){
              poolCue.userData.direction = 0;
            }
            break;
          case 1:
            if(distance >= poolCue.userData.UpperLimit){
              poolCue.userData.direction = -1;
            }
            break;
          default:
            poolCue.userData.direction = 0;
            break;
        }

        poolCue.userData.physicsBody.setLinearVelocity(
          new Ammo.btVector3(poolCue.userData.direction * params.poolCueSpeed, 0, 0)
        );
      }
      
      function lifterWork(){
        const origin = lifter.position.clone();
        origin.x -= getParam('vehicle', 'Length') * 0.5;
        const dest = origin.clone();
        dest.y += getParam('vehicle', 'Height') * 0.5;

        switch(lifter.userData.direction){
          case 0:
            if(origin.y < lifter.userData.UpperLimit * 0.5){
              if(castPhysicsRay(origin, dest))
              {  
                lifter.userData.direction = 1;
                vehicleCanMove = false;

                followedObject = lifter;
              }
              if(Math.abs(bulldozer.rotation.x) <= Math.PI * 0.01){
                bulldozerHinge.enableAngularMotor(true, 0, 1000);
              }
            }
            else if (origin.y > lifter.userData.UpperLimit * 0.5){
              if(!castPhysicsRay(origin, dest))
              {  
                lifter.userData.direction = -1;
                bulldozerHinge.enableAngularMotor(false, 0, 10);
                if(followedObject === lifter)
                  followedObject = dominos[0];
              }
              else{
                lifter.userData.direction = 0;
              }
            }
            
            break;
          case -1:
            if(origin.y <= lifter.userData.LowerLimit){
              lifter.userData.direction = 0;
            }
            break;
          case 1:
            if(origin.y >= lifter.userData.UpperLimit){
              lifter.userData.direction = 0;
              const s = getParam('bulldozer', 'Speed');
              bulldozerHinge.enableAngularMotor(true, s, s*2);
            }
            break;
          default:
            lifter.userData.direction = 0;
            break;
        }
        lifterSpeed = lifter.userData.direction * getParam('lifter', 'Speed');
        lifter.userData.physicsBody.setLinearVelocity(new Ammo.btVector3(0, lifterSpeed, 0));
      }
    
      function updateDominos(threeObject0, threeObject1, contactManifold){

        const userData0 = threeObject0 ? threeObject0.userData : null;
				const userData1 = threeObject1 ? threeObject1.userData : null;

				const breakable0 = userData0 ? userData0.breakable : false;
				const breakable1 = userData1 ? userData1.breakable : false;

				const collided0 = userData0 ? userData0.collided : false;
				const collided1 = userData1 ? userData1.collided : false;

        const rb0 = threeObject0.userData.physicsBody;
				const rb1 = threeObject0.userData.physicsBody;

        if ( ( ! breakable0 && ! breakable1 ) || ( collided0 && collided1 ) ) {

          return;

        }

        let contact = false;
				let maxImpulse = 0;
				for ( let j = 0, jl = contactManifold.getNumContacts(); j < jl; j ++ ) {

					const contactPoint = contactManifold.getContactPoint( j );

					if ( contactPoint.getDistance() < 0 ) {

						contact = true;
						const impulse = contactPoint.getAppliedImpulse();

						if ( impulse > maxImpulse ) {

							maxImpulse = impulse;
							const pos = contactPoint.get_m_positionWorldOnB();
							const normal = contactPoint.get_m_normalWorldOnB();
							impactPoint.set( pos.x(), pos.y(), pos.z() );
							impactNormal.set( normal.x(), normal.y(), normal.z() );

						}

						break;

					}

				}

        // If no point has contact, abort
				if ( ! contact ) return;


        // Subdivision
        // console.log(maxImpulse)
        const fractureImpulse = getParam('domino', 'Impulse');
        if ( breakable0 && ! collided0 && maxImpulse > fractureImpulse ) {

          const debris = convexBreaker.subdivideByImpact( threeObject0, impactPoint, impactNormal, 1, 2, 1.5 );

          const numObjects = debris.length;
          for ( let j = 0; j < numObjects; j ++ ) {

            const vel = rb0.getLinearVelocity();
            const angVel = rb0.getAngularVelocity();
            const fragment = debris[ j ];
            fragment.userData.velocity.set( vel.x(), vel.y(), vel.z() );
            fragment.userData.angularVelocity.set( angVel.x(), angVel.y(), angVel.z() );
            createDebrisFromBreakableObject( fragment );

          }

          objectsToRemove[ numObjectsToRemove ++ ] = threeObject0;
          userData0.collided = true;

        }

        if ( breakable1 && ! collided1 && maxImpulse > fractureImpulse ) {

          const debris = convexBreaker.subdivideByImpact( threeObject1, impactPoint, impactNormal, 1, 2, 1.5 );

          const numObjects = debris.length;
          for ( let j = 0; j < numObjects; j ++ ) {

            const vel = rb1.getLinearVelocity();
            const angVel = rb1.getAngularVelocity();
            const fragment = debris[ j ];
            fragment.userData.velocity.set( vel.x(), vel.y(), vel.z() );
            fragment.userData.angularVelocity.set( angVel.x(), angVel.y(), angVel.z() );

            createDebrisFromBreakableObject( fragment );

          }

          objectsToRemove[ numObjectsToRemove ++ ] = threeObject1;
          userData1.collided = true;

        }
      }

    </script>
  </body>
</html>